Type classes are a functional way to get inheritance like behavior and polymorphism without relying on inheritance. The core idea for type classes is that you separate the behavior from the class that gets effected by it. Before going into type classes in scala, lets go over type classes in java.
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Comparable<T> {
public int compareTo(T o);
}
Most people will be familiar with Comparator
and Comparable
if they have dealt with collection sorting and/or TreeMap
. When working with sorting a collection, you tell the sort method how to compare items together and then sort
will use that to determine order.
Collections.sort(myArray, oddNumbersFirst)
This basic idea gives the developer using these apis flexibility with how to reuse them. In the above example, the array is placing all odd numbers first.
In scala, there is short hand for this kind of behavior; implicits. Lets go over the example above in a more scala friendly way.
trait Comparator[T] {
def compare(t1: T, t2: T): Int
}
// Think of T <: Comparable[T] as weird syntax for "the implementing type"
trait Comparable[T <: Comparable[T]] { self: T =>
def compareTo(t2: T)(implicit comp: Comparator[T]): Int = comp.compare(self, t2)
}
The main change between the Comparable
here and the java one above is that it relies on Comparator
implicitly. How would this look like to the user?
class Age(val age: Int) extends Comparable[Age]
object Age {
def apply(age: Int): Age = new Age(age)
implicit val naturalAgeComparator: Comparator[Age] = new Comparator[Age] {
def compare(t1: Age, t2: Age): Int = t1.age - t2.age
}
}
import Age._
Age(1) compareTo Age(2) // -1
Age(1) compareTo Age(1) // 0
// Age(1) compareTo 1 // won't compile
As we see, the user no longer needs to care about providing a Comparator
since the compiler will do that for us. If we want to override and use a different one, we are still free to (unlike the java one).
Age(1).compareTo(Age(2))(Age.naturalAgeComparator) // -1
Now that we see the core idea, can we find more places where they can come in handy? One easy one is to provide a type safe equals method!
trait Equal[A] {
def equal(a1: A, a2: A): Boolean
}
// acts like the Comparable trait, but doesn't rely on extending the trait
implicit class EqualOpt[A](val self: A) extends AnyVal {
def ===(a2: A)(implicit eq: Equal[A]): Boolean = eq.equal(self, a2)
def =/=(a2: A)(implicit eq: Equal[A]): Boolean = !eq.equal(self, a2)
}
class Foo(val name: String)
object Foo {
def apply(name: String): Foo = new Foo(name)
implicit val naturalEqual: Equal[Foo] = new Equal[Foo] {
def equal(a1: Foo, a2: Foo): Boolean = a1.name == a2.name
}
}
import Foo._
Foo("hi") === Foo("there") // returns false
Foo("hi") === Foo("hi") // returns true
// Foo("hi") === "1" // compiler rejects this
What about serialization to json, protobuf, etc.?
trait Serialize[T, O] {
def serialize(t: T): O
}
implicit class SerializeOpt[A](val self: A) extends AnyVal {
def serialize[O](implicit ser: Serialize[A, O]): O = ser.serialize(self)
}
class Node(val value: String)
object Node {
type Json = String
type Length = Int
implicit val jsonSer: Serialize[Node, Json] = new Serialize[Node, Json] {
def serialize(t: Node): Json = s"{'value': '${t.value}'}"
}
implicit val lengthSer: Serialize[Node, Length] = new Serialize[Node, Length] {
def serialize(t: Node): Length = t.value.length
}
}
import Node._
new Node("hi").serialize[Json] // {'value': 'hi'}
new Node("hi").serialize[Length] // 2